/*
	millerbnc - Simplistic IRC bouncer

	Copyright (C) 2007 Christopher E. Miller

	This program is free software; you can redistribute it and/or modify
	it under the terms of the GNU General Public License as published by
	the Free Software Foundation; version 2 of the License.

	This program is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
	GNU General Public License for more details.

	You should have received a copy of the GNU General Public License
	along with this program; if not, write to the Free Software
	Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/


module millerbnc;

import std.socket, std.stream, std.string, std.conv,
	std.random, std.stdio;
import splat;
import irclib.all, irclib.splatclient;


extern(C)
{
	alias uint c_time_t;
	c_time_t time(c_time_t*);
	alias time c_time;
}


version(Windows)
{
}
else
{
	import std.c.linux.linux;
	
	private extern(C)
	{
		FILE* popen(char* command, char* type);
		int pclose(FILE* stream);
		
		alias int pid_t;
		
		pid_t getppid();
		pid_t getpid();
		
		pid_t setsid();
		
		alias void function(int) sig_t;
		sig_t signal(int sig, sig_t func);
		
		const sig_t SIG_IGN = cast(sig_t)1;
		
		int kill(pid_t pid, int sig);
	}
}


bool toBool(char[] s)
{
	if(!s.length || "0" == s || !std.string.icmp("no", s) || !std.string.icmp("false", s))
		return false;
	return true;
}


class Config
{
	char[] name, password;
	char[] detachaway;
	char[] logdir;
	ushort pingtime = 20, pingtimeout = 90;
	bool logsincelastping = true;
	bool logshowdate = true;
	bool admin = false;
}


const ushort DEFAULT_PORT = 16667;
const ushort DEFAULT_LOGSEND_DCC_PORT = 16667;


char[] configfilename = "millerbnc.conf";
Config[] uconfigs;
uint listenbind = InternetAddress.ADDR_ANY;
ushort listenport = DEFAULT_PORT;
ushort logsenddccport = DEFAULT_LOGSEND_DCC_PORT;
ushort maxclients = 8;
bool daemon = true;
uint ircmaxreadbuf = 16 * 1024; // ircuser <- ircserver
uint ircmaxwritebuf = 24 * 1024; // ircuser -> ircserver
uint usermaxreadbuf = 16 * 1024; // pclient <- ircuser
uint usermaxwritebuf = 16 * 1024; // pclient -> ircuser

ushort numPSockets = 0;


class BTimer: Timer
{
	void delegate(Timer) callback;
	
	
	final void setCallback(void delegate(Timer) callback)
	{
		assert(!this.callback);
		this.callback = callback;
	}
	
	
	protected override void onAlarm()
	{
		if(callback)
			callback(this);
	}
}


void loadConfig(Stream stm)
{
	uconfigs = null;
	
	Config uc;
	char[] s;
	int i;
	foreach(char[] line; stm) // Warning: always dup the line if saved.
	{
		line = std.string.stripl(line);
		if(!line.length || '#' == line[0])
			continue;
		
		if('[' == line[0])
		{
			line = std.string.stripr(line[1 .. $]);
			if(line.length && ']' == line[$ - 1])
			{
				line = line[0 .. $ - 1];
				if(line.length)
				{
					if(uc)
						uconfigs ~= uc;
					uc = new Config();
					uc.name = line.dup;
				}
			}
			continue;
		}
		
		i = std.string.find(line, '=');
		if(-1 != i)
		{
			s = line[i + 1 .. $];
			switch(std.string.tolower(line[0 .. i]))
			{
				case "daemon":
					daemon = toBool(s);
					break;
				
				case "pingtime":
					if(uc)
						uc.pingtime = std.conv.toUshort(s);
					break;
				
				case "pingtimeout":
					if(uc)
						uc.pingtimeout = std.conv.toUshort(s);
					break;
				
				case "detachaway":
					if(uc)
						uc.detachaway = s.dup;
					break;
				
				case "password":
					if(uc)
					{
						if(!std.string.icmp(uc.name, "joe") && s == "shmoe")
							throw new Exception("Fix the configuration; don't leave the default user name and password");
						uc.password = s.dup;
					}
					break;
				
				case "admin":
					if(uc)
						uc.admin = toBool(s);
					break;
				
				case "logdir":
					if(uc)
						uc.logdir = s.dup;
					break;
				
				case "logshowdate":
					if(uc)
						uc.logshowdate = toBool(s);
					break;
				
				case "maxclients":
					maxclients = std.conv.toUshort(s);
					break;
				
				case "ircmaxreadbuf":
					ircmaxreadbuf = std.conv.toUint(s);
					break;
				
				case "ircmaxwritebuf":
					ircmaxwritebuf = std.conv.toUint(s);
					break;
				
				case "usermaxreadbuf":
					usermaxreadbuf = std.conv.toUint(s);
					break;
				
				case "usermaxwritebuf":
					usermaxwritebuf = std.conv.toUint(s);
					break;
				
				case "listen":
					i = std.string.find(s, ':');
					if(-1 != i)
					{
						listenbind = InternetAddress.parse(s[0 .. i]);
						if(InternetAddress.ADDR_NONE == listenbind)
						{
							scope ih = new InternetHost();
							if(!ih.getHostByName(s[0 .. i]) || !ih.addrList.length) // Note: blocking.
								throw new Exception("Unable to resolve host " ~ s[0 .. i]);
							listenbind = ih.addrList[std.random.rand() % ih.addrList.length];
						}
						s = s[i + 1 .. $];
					}
					listenport = std.conv.toUshort(s);
					break;
				
				default: ;
			}
		}
	}
	if(uc)
		uconfigs ~= uc; // Last one with none next.
	
	// All went well, need to update the Config's for all current ircusers...
	// If something went wrong and an exception was thrown, the current users
	// continue to use the old Config's until another loadConfig call succeeds.
	foreach(iu; ircusers)
	{
		if(iu.config)
		{
			foreach(cf; uconfigs)
			{
				if(!std.string.icmp(iu.config.name, cf.name))
				{
					iu.config = cf;
					break;
				}
			}
		}
	}
}


void loadConfig(char[] file)
{
	scope bstm = new BufferedFile(file);
	scope(exit)
		bstm.close();
	
	scope estm = new EndianStream(bstm);
	scope(exit)
		estm.close();
	
	estm.readBOM();
	loadConfig(estm);
}


IrcUser[] ircusers; // Note: mutable.


struct RecentLog
{
	char[] line;
	c_time_t when;
}


class IrcUser: SplatIrcClient
{
	Config config;
	PClient pclient;
	//char[] nick; // Shadows IRC nick...
	char[][] chans; // Note: mutable.
	char[] cookie;
	char[] _fulladdr; // Not realiable; use fulladdress
	char[][] firstcmds;
	
	Stream log;
	char[][] logsinceping;
	RecentLog[] rlog;
	
	
	char[] fulladdress() // getter
	{
		if(!_fulladdr.length)
			return nick;
		assert(_fulladdr.length >= nick.length && !this.strcmp(_fulladdr[0 .. nick.length], nick)); // Note: this can fail on a goofy network.
		return _fulladdr;
	}
	
	
	protected override void onConnected()
	{
		super.onConnected();
		
		ircusers ~= this;
		
		if(pclient)
		{
			char[] s = pclient.ircuserUnsentClear();
			if(s.length)
				_sendraw(s);
		}
	}
	
	
	protected override void onDisconnected()
	{
		super.onDisconnected();
		
		foreach(idx, iu; ircusers)
		{
			if(iu is this)
			{
				ircusers[idx] = ircusers[$ - 1];
				ircusers = ircusers[0 .. $ - 1];
				break;
			}
		}
		
		if(pclient)
		{
			pclient.sendLine("ERROR :Closing Link: 127.0.0.1 (Disconnected from server)");
			pclient.sendClose();
			pclient = null; // ?
		}
	}
	
	
	// "Send USER and NICK commands."
	protected override void sendLoginInfo(char[] serverHost)
	{
		// Don't send this, the pclient will!
	}
	
	
	private void _sendraw(char[] raw)
	{
		if(queue.writeBytes + raw.length > ircmaxwritebuf)
		{
			// Error: flood.
			
			if(pclient)
			{
				pclient.sendLine("ERROR :Closing Link: 127.0.0.1 (Disconnecting from server (excess send queue to server))");
				pclient.sendClose();
				pclient = null; // Prevent onDisconnected from sending another quit.
			}
			
			this.abort();
			//queue.clear();
		}
		else
		{
			queue.write(raw);
		}
	}
	
	
	override void sendLine(char[] line)
	{
		_sendraw(line ~ "\r\n");
	}
	
	
	//firstcmds
	protected override void onCommand(char[] prefix, char[] cmd, char[] cmdParams)
	{
		super.onCommand(prefix, cmd, cmdParams);
		
		switch(cmd)
		{
			case "001":
			case "002":
			case "003":
			case "004":
			case "005":
				if(firstcmds.length < 8) // Allow a few extra since sometimes 005 spans a few lines.
					firstcmds ~= ":" ~ prefix ~ " " ~ cmd ~ " " ~ cmdParams;
				break;
			
			default: ;
		}
	}
	
	
	protected override void onLine(char[] line)
	{
		char[] s;
		
		super.onLine(line);
		
		debug(BNCPRINT)
			writefln(" * from ircserver < %s", line);
		
		if(pclient)
		{
			// super.onLine handles PING; let the bouncer do it as it'll always have the best opportunity.
			s = line;
			if(s.length && ':' == s[0])
				nextWord(s);
			if(!std.string.icmp("PING", nextWord(s)))
				return;
			
			pclient.sendLine(line);
		}
	}
	
	
	protected void onCtcpReceived(CtcpMessage cmsg)
	{
		if(!std.string.icmp(cmsg.ctcp, "PING"))
		{
			char[] msg;
			msg = cmsg.message;
			if(msg.length > 32)
				msg = msg[0 .. 32];
			sendCtcpReply(cmsg.fromNick, "PING", msg);
		}
	}
	
	
	private void _removechan(char[] name)
	{
		foreach(idx, chan; chans)
		{
			if(!this.strcmp(chan, name))
			{
				/+ // I'd rather keep them in join order, for a better attach join order.
				chans[idx] = chans[$ - 1];
				chans = chans[0 .. $ - 1];
				+/
				if(chans.length <= 1)
					chans = null;
				else if(idx == chans.length - 1)
					chans = chans[0 .. $ - 1];
				else if(!idx)
					chans = chans[1 .. $];
				else
					chans = chans[0 .. idx] ~ chans[idx + 1 .. $];
				break;
			}
		}
	}
	
	
	private void _addchan(char[] name)
	{
		foreach(idx, chan; chans)
		{
			if(!this.strcmp(chan, name))
				return;
		}
		chans ~= name;
	}
	
	
	protected void onNick(IrcNick inick)
	{
		if(!this.strcmp(nick, inick.fromNick))
		{
			_fulladdr = inick.from;
		}
	}
	
	
	protected override void onChannelJoin(ChannelJoin cjoin)
	{
		if(!this.strcmp(nick, cjoin.fromNick))
		{
			_fulladdr = cjoin.from;
			
			_addchan(cjoin.channelName);
		}
	}
	
	
	protected override void onChannelPart(ChannelPart cpart)
	{
		if(!this.strcmp(nick, cpart.fromNick))
			_removechan(cpart.channelName);
	}
	
	
	protected override void onChannelKick(ChannelKick ckick)
	{
		if(!this.strcmp(nick, ckick.kickedNick))
			_removechan(ckick.channelName);
	}
	
	
	protected override void waitForEvent()
	{
		super.waitForEvent();
		
		if(queue.readBytes > ircmaxreadbuf)
		{
			// Error: flood.
			
			if(pclient)
			{
				pclient.sendLine("ERROR :Closing Link: 127.0.0.1 (Disconnecting from server (excess send queue to server))");
				pclient.sendClose();
				pclient = null; // Prevent onDisconnected from sending another quit.
			}
			
			this.abort();
			//queue.clear();
		}
	}
}


class PClient: PSocket
{
	Config config;
	SocketQueue _queue;
	bool _attaching = false;
	char[] _gotuser, _gotnick;
	
	IrcUser ircuser;
	char[] ircunsent;
	
	c_time_t conntime, lastpingtime;
	
	
	void sendRaw(void[] raw)
	{
		if(_queue.sendBytes + raw.length > usermaxwritebuf)
		{
			// Error: flood.
			close();
			//_queue.clear();
		}
		else
		{
			_queue.send(raw);
		}
	}
	
	
	void sendLine(char[] line)
	{
		debug(BNCPRINT)
			writefln(" * to pclient > %s", line);
		
		sendRaw(line ~ "\r\n");
	}
	
	
	private char[] ircuserUnsentClear()
	{
		char[] s = ircunsent;
		ircunsent = null;
		return s;
	}
	
	
	void ircuserSendLine(char[] line)
	{
		debug(BNCPRINT)
			writefln(" * to ircserver > %s", line);
		
		if(!ircuser || !ircuser.isConnected)
		{
			ircunsent ~= line;
			ircunsent ~= "\r\n";
			return;
		}
		
		if(ircunsent.length)
		{
			ircuser._sendraw(ircunsent);
			ircunsent = null;
		}
		
		ircuser.sendLine(line);
	}
	
	
	private this()
	{
		_queue = new SocketQueue(this);
		event(EventType.CLOSE | EventType.READ | EventType.WRITE, &onPClientEvent);
		
		linger lng;
		lng.on = true;
		lng.time = 4;
		setOption(SocketOptionLevel.SOCKET, SocketOption.LINGER, lng);
		
		numPSockets++;
		_nnn = false;
		
		lastpingtime = conntime = c_time(null);
	}
	
	
	private char[][] _attachchans;
	
	
	private bool _doattachone()
	{
		if(!_attachchans.length)
			return false;
		
		char[] chan = _attachchans[0];
		_attachchans = _attachchans[1 .. $];
		
		//writefln(" * attach:join %s", chan);
		
		sendLine(":" ~ ircuser.fulladdress ~ " JOIN " ~ chan);
		ircuserSendLine("NAMES " ~ chan);
		ircuserSendLine("MODE " ~ chan);
		ircuserSendLine("TOPIC " ~ chan);
		
		return true;
	}
	
	
	private void _attachTimeout(Timer tmr)
	{
		try
		{
			if(!_doattachone())
				tmr.stop();
		}
		catch
		{
			tmr.stop();
		}
	}
	
	
	protected void gotUserAndNick()
	{
		assert(ircuser !is null);
		
		if(_attaching)
		{
			_attaching = false;
			
			// Send fake server stuff; important to give the client their real nickname, and other reasons.
			//sendLine(":foo.bar 001 " ~ ircuser.nick ~ " :You have reattached to your previous IRC session."); // firstcmds has the server's.
			foreach(char[] fc; ircuser.firstcmds)
			{
				sendLine(fc);
			}
			sendLine(":foo.bar 422 " ~ ircuser.nick ~ " :No MOTD"); // Important. Either this or request the current MOTD from the server...
			
			ircuserSendLine("MODE " ~ ircuser.nick); // Get user mode.
			
			// Send me JOINs for all the channels I'm actually on.
			if(1 == ircuser.chans.length)
			{
				_attachchans = ircuser.chans; // Use this array since it's just one done now.
				_doattachone();
				assert(!_attachchans.length);
				_attachchans = null;
			}
			else if(ircuser.chans.length)
			{
				_attachchans = ircuser.chans.dup; // Dup because chans is mutable.
				_doattachone();
				
				with(new BTimer())
				{
					interval = 1000; // Can get the user flooded off if this is too low.
					setCallback(&_attachTimeout);
					start();
				}
			}
			//else == 0 chans.
		}
	}
	
	
	private void _checkstart()
	{
		if(_gotuser && _gotnick && ircuser)
			gotUserAndNick();
	}
	
	
	void noticeToPClientFromBnc(char[] msg)
	{
		sendLine(":-bnc!bnc@foo.bar NOTICE " ~ ircuser.nick ~ " :" ~ msg);
	}
	
	
	void messageToPClientFromBnc(char[] msg)
	{
		sendLine(":-bnc!bnc@foo.bar PRIVMSG " ~ ircuser.nick ~ " :" ~ msg);
	}
	
	
	//gotBncCommand(prefix, s, &sendMessage);
	void gotBncCommand(char[] prefix, char[] cmdParams, void delegate(char[] msg) reply)
	{
		// Note: ircuser isn't guaranteed to be set.
		
		void denied() { reply("Access is denied"); }
		
		char[] cmd = nextWord(cmdParams);
		if(!std.string.icmp(cmd, "quit"))
		{
			if(ircuser)
			{
				if(ircuser.cookie.length)
					sendLine("ERROR :Closing Link: 127.0.0.1 (Disconnecting from server (not detaching with cookie))");
				else
					sendLine("ERROR :Closing Link: 127.0.0.1 (Disconnecting from server)");
				
				ircuserSendLine("QUIT :" ~ cmdParams);
			}
			
			sendClose();
		}
		else if(!std.string.icmp(cmd, "rehash"))
		{
			if(!config || !config.admin)
			{
				denied();
			}
			else
			{
				reply("Rehashing configuration file...");
				loadConfig(configfilename);
				reply("Configuration loaded");
			}
		}
		else if(!std.string.icmp(cmd, "set"))
		{
			if(!ircuser)
			{
				// Must login first...
			}
			else
			{
				char[] setcmd = nextWord(cmdParams);
				if(!std.string.icmp(setcmd, "cookie"))
				{
					if(!cmdParams.length)
					{
						if(ircuser.cookie.length)
							reply("Cookie for this session is: " ~ ircuser.cookie);
						else
							reply("No cookie for this session");
					}
					else if("-" == cmdParams)
					{
						ircuser.cookie = null;
						reply("Cookie cleared for this session");
					}
					else
					{
						ircuser.cookie = nextWord(cmdParams).dup;
						reply("Cookie for this session is now: " ~ ircuser.cookie);
					}
				}
				else
				{
					reply("Unknown setting: " ~ setcmd);
				}
			}
		}
		else
		{
			reply("Unknown command: " ~ cmd);
		}
	}
	
	
	protected void onCommand(char[] prefix, char[] cmd, char[] cmdParams, char[] wholeline)
	{
		char[] s;
		int i;
		
		// Note: only the last 'else' case sends the commands to the IRC server.
		
		if(!std.string.icmp(cmd, "user"))
		{
			if(!_attaching)
				ircuserSendLine(wholeline);
			
			if(!_gotuser)
			{
				//_gotuser = true;
				_gotuser = cmdParams;
				_checkstart();
			}
		}
		else if(!std.string.icmp(cmd, "nick"))
		{
			if(!_attaching)
				ircuserSendLine(wholeline);
			
			if(!_gotnick)
			{
				//_gotnick = true;
				_gotnick = cmdParams;
				_checkstart();
			}
		}
		else if(!std.string.icmp(cmd, "quit"))
		{
			if(!ircuser)
			{
				ircuserSendLine(wholeline);
			}
			else if(ircuser.cookie.length)
			{
				sendLine("ERROR :Closing Link: 127.0.0.1 (Detaching with cookie: " ~ ircuser.cookie ~ ")");
				
				if(config && config.detachaway.length)
					ircuserSendLine("AWAY :" ~ config.detachaway);
			}
			else
			{
				sendLine("ERROR :Closing Link: 127.0.0.1 (Disconnecting from server)");
				ircuserSendLine(wholeline);
			}
			
			sendClose();
		}
		else if(!std.string.icmp(cmd, "bnc") || !std.string.icmp(cmd, "-bnc"))
		{
			gotBncCommand(prefix, cmdParams, &noticeToPClientFromBnc);
		}
		else if(!std.string.icmp(cmd, "privmsg"))
		{
			s = cmdParams;
			if(!std.string.icmp("-bnc", ircParam(s)))
			{
				gotBncCommand(prefix, ircParam(s), &messageToPClientFromBnc);
			}
			else
			{
				ircuserSendLine(wholeline);
			}
		}
		else if(!std.string.icmp(cmd, "notice"))
		{
			s = cmdParams;
			if(!std.string.icmp("-bnc", ircParam(s)))
			{
				gotBncCommand(prefix, ircParam(s), &noticeToPClientFromBnc);
			}
			else
			{
				ircuserSendLine(wholeline);
			}
		}
		else
		{
			if(!ircuser)
			{
				if(!std.string.icmp(cmd, "pass"))
				{
					s = cmdParams;
					i = std.string.find(s, ";;");
					if(-1 != i)
					{
						wholeline = "PASS " ~ s[i + 2 .. $]; // Note: resetting wholeline.
						ircuserSendLine(wholeline);
						s = s[0 .. i];
					}
					
					char[][] pp = std.string.split(s, ";");
					if(pp.length < 2)
					{
						sendLine(":foo.bar 464 * :Invalid format for PASS");
					}
					else
					{
						ushort nport = 6667;
						char[] snhost;
						
						s = pp[1];
						i = std.string.find(s, ':');
						if(-1 != i)
						{
							try
							{
								nport = std.conv.toUshort(s[i + 1 .. $]);
							}
							catch
							{
								nport = 0;
							}
							s = s[0 .. i];
						}
						
						if(!nport)
						{
							sendLine(":foo.bar 464 * :Invalid port in PASS");
						}
						else
						{
							snhost = s;
							i = std.string.find(pp[0], ':');
							if(-1 == i)
							{
								sendLine(":foo.bar 464 * :I need the password!");
							}
							else
							{
								char[] n, p;
								n = pp[0][0 .. i];
								p = pp[0][i + 1 .. $];
								
								char[] xcookie;
								if(pp.length >= 3)
								{
									xcookie = pp[2].dup;
									
									foreach(iu; ircusers)
									{
										if(iu.cookie == xcookie)
										{
											if(iu.config
												&& !std.string.icmp(iu.config.name, n)
												&& iu.config.password == p)
											{
												if(iu.pclient)
												{
													/+
														/+ // This code causes the following...
															 * from pclient < PASS foo:bar;208.75.85.212;mmcookie
																Error: Access Violation
														+/
													iu.pclient.sendLine("ERROR :Closing Link: 127.0.0.1 (User attached from another location)");
													iu.pclient.sendClose();
													
													iu.pclient.ircuser = null;
													+/
													sendLine(":foo.bar 464 * :Credentials match existing user but cannot take over session; please try again later");
													sendClose(); // Close them so they can auto-reconnect and try again.
													goto bad_cred;
												}
												
												ircuser = iu;
												config = iu.config;
												_attaching = true;
											}
											else
											{
												sendLine(":foo.bar 464 * :Credentials do not match existing user!");
												goto bad_cred;
											}
										}
									}
								}
								
								if(!ircuser)
								{
									// Find config.
									foreach(uc; uconfigs)
									{
										if(!std.string.icmp(n, uc.name))
										{
											if(p == uc.password)
											{
												config = uc;
												break;
											}
										}
									}
									
									if(!config)
									{
										sendLine(":foo.bar 464 * :Invalid credentials!");
										sendClose();
									}
									else
									{
										ircuser = new IrcUser();
										ircuser.nick = "millerbnc"; // Shouldn't ever be used..
										ircuser.config = config;
										ircuser.cookie = xcookie;
										ircuser.pclient = this;
										
										ircuser.serverHost = snhost;
										ircuser.serverPort = nport;
										
										ircuser.connect();
										_checkstart();
									}
								}
								else // Attaching...
								{
									ircuser.pclient = this;
									
									ircuserSendLine("AWAY"); // Come back; always send this in case they had away-on-detach and the config changed, etc.
									// Note: other resync stuff should be done after pclient sends bogus NICK/USER ... trust me.
								}
								
								bad_cred: ;
							}
						}
					}
				}
				else
				{
					sendLine(":foo.bar 464 * :I need the password!");
				}
			}
			else
			{
				ircuserSendLine(wholeline);
			}
		}
	}
	
	
	private void _docmd(char[] line)
	{
		char[] wholeline = line;
		char[] prefix;
		char[] cmd;
		cmd = nextWord(line);
		if(!cmd.length)
			return;
		if(cmd[0] == ':')
		{
			if(cmd.length == 1)
				return;
			prefix = cmd[1 .. cmd.length];
			cmd = nextWord(line);
			if(!cmd.length)
				return;
		}
		onCommand(prefix, cmd, line, wholeline);
	}
	
	
	protected void onLine(char[] line)
	{
		if(_sendclosing)
			return;
		
		debug(BNCPRINT)
			writefln(" * from pclient < %s", line);
		
		_docmd(line);
	}
	
	
	private void gotReadEvent()
	{
		byte[] peek;
		find_line:
		peek = cast(byte[])_queue.peek();
		foreach(idx, b; peek)
		{
			if('\r' == b || '\n' == b)
			{
				if(!idx)
				{
					_queue.receive(1); // Remove from queue.
					goto find_line;
				}
				_queue.receive(idx + 1); // Remove from queue.
				onLine(cast(char[])peek[0 .. idx]);
				goto find_line;
			}
		}
	}
	
	
	private void onPClientEvent(Socket sock, EventType type, int err)
	{
		if(err)
		{
			close();
			return;
		}
		
		switch(type)
		{
			case EventType.READ:
				_queue.readEvent();
				if(_queue.receiveBytes > usermaxreadbuf)
				{
					// Error: flood.
					close();
					//_queue.clear();
				}
				else
				{
					gotReadEvent();
				}
				break;
			
			case EventType.WRITE:
				_queue.writeEvent();
				if(_sendclosing && !_queue.sendBytes)
				{
					close();
				}
				break;
			
			case EventType.CLOSE:
				close();
				break;
			
			default: ;
		}
	}
	
	
	override void close()
	{
		if(ircuser)
			ircuser.pclient = null;
		
		if(!_nnn)
		{
			debug(BNCPRINT)
				writefln("PClient socket closed");
			
			_nnn = true;
			if(numPSockets)
				numPSockets--;
		}
		
		super.close();
	}
	
	
	void sendClose()
	{
		_sendclosing = true;
	}
	
	
	private:
	
	bool _nnn = true;
	bool _sendclosing = false;
}


class PSocket: AsyncTcpSocket
{
	this()
	{
	}
	
	
	private void onPServerEvent(Socket sock, EventType type, int err)
	{
		if(EventType.ACCEPT == type)
		{
			auto asock = accept();
			if(numPSockets >= maxclients)
			{
				asock.close();
				return;
			}
		}
	}
	
	
	protected override PSocket accepting()
	{
		return new PClient();
	}
}


PSocket mainsock;


int main(char[][] args)
{
	// Do init stuff before turning into daemon so it can show the errors nicely.
	
	if(args.length > 1)
		configfilename = args[1];
	loadConfig(configfilename);
	
	mainsock = new PSocket();
	mainsock.event(EventType.ACCEPT, &mainsock.onPServerEvent);
	mainsock.bind(new InternetAddress(listenbind, listenport));
	mainsock.listen(4);
	
	if(daemon)
	{
		version(Windows)
		{
		}
		else
		{
			if(1 != getppid())
			{
				writefln("Turning into daemon... (ignore the kill)");
				
				int i;
				
				i = fork();
				if (i < 0)
					throw new Exception("Unable to fork");
				if(i > 0)
				{
					// Parent needs to exit, but do NOT clean up (don't call dtors, etc)
					kill(getpid(), 9);
					throw new Exception("kill 9 did not work");
					return 1;
				}
				
				// Now running as child.
				
				setsid(); // New process group.
				
				// Close std* fd`s.
				close(0);
				close(1);
				close(2);
				
				// New std I/O.
				i = open("/dev/null", O_RDWR);
				dup(i);
				dup(i);
				
				//umask(027); // Restrict file creation mode to 750 (complement of 027).
				
				// Ignore a few signals.
				signal(SIGCHLD, SIG_IGN); // Ignore child.
				signal(SIGTSTP, SIG_IGN); // Ignore tty signals.
				signal(SIGTTOU, SIG_IGN);
				signal(SIGTTIN, SIG_IGN);
			}
			
			signal(SIGPIPE, SIG_IGN);
		}
	}
	
	uint numerrors = 0;
	bool done = false;
	do
	{
		try
		{
			splat.run();
			done = true;
		}
		catch(Object e)
		{
			writefln("Error: %s", e.toString());
			
			try
			{
				// Tell all clients...
				foreach(iu; ircusers)
				{
					if(iu.pclient)
						iu.pclient.noticeToPClientFromBnc("Error: " ~ e.toString());
				}
			}
			catch
			{
			}
			
			if(++numerrors >= 20)
			{
				// Tell all clients exiting due to issue... ? Can't because it's aborting and can't wait?
				
				throw e;
			}
		}
	}
	while(!done);
	
	return 0;
}

